/*
 * ﻿Copyright (C) 2011-2012 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.aspectjutil.adviseutil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;

/**
 * Utility class for AspectJ's advice methods 
 * 
 * @author <a href="mailto:artymt@gmail.com">Arata Y.</a>
 */
public class Util {
	private static Logger logger = Logger.getLogger( Util.class.getName());
		protected static Logger getLogger() {
			return logger;
		}

	/**
	 * Check whether given <code>throwable</code> object is type of ones defined at throws clause of 
	 * method signature 
	 * @param methodSignature signature from cross-cut method
	 * @param throwable to be checked whether defined by <code>methodSignature</code> input 
	 * as possible to be thrown
	 * @return <code>true</code> when type of <code>throwable</code> input is found by <code>methodSignature</code> 
	 * input as possible to be thrown. 
	 * <code>false</code> will be returned too when either <code>methodSignature</code> input or 
	 * <code>throwable</code> input is null. 
	 */
	public static boolean isExceptionChecked( 
			final MethodSignature methodSignature, final Throwable throwable) 
	{
		if ( methodSignature == null) {
			Logger logger = Util.getLogger();
			if ( logger.isLoggable( Level.INFO)) {
				logger.info( "methodSignature input is null, thereby returning false ");
			}
			return false;
		}
		if ( throwable == null) {
			Logger logger = Util.getLogger();
			if ( logger.isLoggable( Level.INFO)) {
				logger.info( "throwable input is null, thereby returning false ");
			}
			return false;
		}
		
		if ( throwable instanceof RuntimeException) return true;
		
		Class<?> throwableClass = throwable.getClass();
		for( Class<?> exceptionType : methodSignature.getExceptionTypes()) {
			if ( exceptionType.isAssignableFrom( throwableClass)) {
				return true;
			}
		} // for
		
		return false;
	}
	
	/**
	 * Extract {@link StackTraceElement} objects out of <code>stackTraceElementArray</code> input 
	 * excluding reflective invocation one.
	 * @param stackTraceElementArray what <code>StackTraceElement</code> objects are extracted from.
	 * @param from specify starting point of extracting out of <code>stackTraceElementArray</code>. 
	 * Usually needs to skip first 5 elements: first 2 elements are about trace of method being called, 
	 * and next 3 elements are by aspectj's advice method.
	 * @param count Number of elements to pick up after skipping the number of elements 
	 * specified by "<code>from</code>" argument.
	 * @return {@link List} of {@link StackTraceElement} object excluding reflective invocation one.
	 */
	public static List<String> getCallingMethodInfo( 
			final StackTraceElement[] stackTraceElementArray, int from, int count) 
	{
		List<String> stackTraceElementStringList = new ArrayList<String>();
		
		if ( from < 0) return stackTraceElementStringList;
		if ( count < 1) return stackTraceElementStringList;
		
		for( 
				int stackTraceElementIndex = from; 
				stackTraceElementIndex < stackTraceElementArray.length; 
				stackTraceElementIndex++) 
		{
			String className = stackTraceElementArray[ stackTraceElementIndex].getClassName();
			if ( className.startsWith( "sun.reflect") || className.startsWith( "java.lang.reflect"))
			{
				continue;
			}
			// Store 2 elements about call before reflective invocation in stackTraceElementArray 
			// into stackTraceElementStringBuilder 
			String message 
			= String.format(
					"StackTraceElement[ %1$d]: %2$s%n",
					stackTraceElementIndex,
					stackTraceElementArray[ stackTraceElementIndex].toString()
					);
			stackTraceElementStringList.add( message);
			if ( stackTraceElementStringList.size() >= count) break; // for
		} // for
		
		return stackTraceElementStringList;
	} // public static List<String> getCallingMethodInfo( final StackTraceElement[] stackTraceElementArray, int from, int count) 
	
	/**
	 * Extract calling sequence to <code>joinPoint</code> input in <code>stackTraceElementArray</code> input 
	 * excluding reflective invocation one.
	 * @param stackTraceElementArray what calling sequence information is extracted from. 
	 * The first 2 elements will be ignored automatically since the first element is for 
	 * getStackTrace execution and the next elements is about trace of calling to method 
	 * in where stacktrace was captured
	 * @param joinPoint being used as base of call sequence to find from 
	 * <code>stackTraceElementArray</code> input 
	 * @param count max number of extracted {@link StackTraceElement} object out of  
	 * <code>stackTraceElementArray</code> input
	 * @return {@link List} of {@link StackTraceElement} object traced from <code>joinPoint</code> input 
	 * excluding reflective invocation one.
	 */
	public static List<String> getCallingMethodInfo( 
			final StackTraceElement[] stackTraceElementArray, final JoinPoint joinPoint, int count) 
	{
		List<String> stackTraceElementStringList = new ArrayList<String>();
		
		// arguments validation -------------------------------------------------------------------
		if ( stackTraceElementArray == null) {
			throw new IllegalArgumentException( "stackTraceElementArray argument cannot be null");
		}
		if ( joinPoint == null) {
			throw new IllegalArgumentException( "joinPoint argument cannot be null");
		}
		if ( count < 1) {
			throw new IllegalArgumentException( "value of count argument cannot be smaller than 1");
		}
		
/*		if ( !joinPoint.getKind().startsWith( "method-") && !joinPoint.getKind().startsWith( "constructor-")) {
			return stackTraceElementStringList;
		}
*/
		if ( 
				!joinPoint.getKind().equals( JoinPoint.METHOD_CALL)
				&& !joinPoint.getKind().equals( JoinPoint.METHOD_EXECUTION)
				&& !joinPoint.getKind().equals( JoinPoint.CONSTRUCTOR_CALL)
				&& !joinPoint.getKind().equals( JoinPoint.CONSTRUCTOR_EXECUTION)
				) 
		{
			return stackTraceElementStringList;
		}
		// ----------------------------------------------------------------------------------------
		
		String methodName = joinPoint.getSignature().getName();
		int indexAtTarget = stackTraceElementArray.length;
		boolean indexAtTargetSetFlag = false;
		
		String joinPointClassName;
		
		// By call pointcuts on constructor, joinPoint.getTarget() is null
		if ( joinPoint.getTarget() != null) {
			joinPointClassName = joinPoint.getTarget().getClass().getName();
			
			for( int stackIndex = 2; stackIndex < stackTraceElementArray.length; stackIndex++) {
				/* Skip first 2 elements in stackTraceElementArray because:
				 * 	first element is from getStackTrace execution
				 * 	next elements is about trace of calling to method in where stacktrace was captured
				 */
				
				String className = stackTraceElementArray[ stackIndex].getClassName();
				if ( 
						joinPointClassName.equals( className) 
						&& methodName.equals( stackTraceElementArray[ stackIndex].getMethodName())
						)
				{
					/* logic reaches here when one among following cases 
					 * 	- before-advise on execution pointcuts case
					 * 	- after-advise on execution pointcuts case
					 * 	- around-advise on execution pointcuts case
					 * Interesting thing is that joinPoint.getTarget is in stacktrace even either cases of 
					 * getting stacktrace before or after invoking proceedingJoinPoint.proceed(). 
					 */
					if ( !indexAtTargetSetFlag) {
						indexAtTarget = stackIndex;
						indexAtTargetSetFlag = true;
						continue;
					}
				}
				if ( stackIndex > indexAtTarget) {
					if ( 
							className.startsWith( "sun.reflect") 
							|| className.startsWith( "java.lang.reflect")
							)
					{
						continue;
					}
					// Store stacked invocation info before reflective invocation 
					// in stackTraceElementArray into stackTraceElementStringBuilder 
					String message = String.format(
							"StackTraceElement[ %1$d]: %2$s%n",
							stackIndex,
							stackTraceElementArray[ stackIndex].toString()
							);
					stackTraceElementStringList.add( message);
					if ( stackTraceElementStringList.size() >= count) break; // for
				}
			} // for
		}
		
		if ( !indexAtTargetSetFlag) { // Target of pointcuts is not in stacktrace
			/* When logic reaches here, it's one among following cases:
			 * 	- before-advise on call pointcuts
			 * 	- after-advise on call pointcuts
			 * 	- around-advise on call pointcuts
			 * and joinPoint.getTarget won't be in stacktrace.
			 */
			if ( joinPoint.getThis() != null) { 
				joinPointClassName = joinPoint.getThis().getClass().getName();

				for( int stackIndex = 2; stackIndex < stackTraceElementArray.length; stackIndex++) {
					/* Skip first 2 elements in stackTraceElementArray because:
					 * 	first element is from getStackTrace execution
					 *  next elements is, in shortest path, about trace of advise method. 
					 */
					
					String className = stackTraceElementArray[ stackIndex].getClassName();
					if ( joinPointClassName.equals( className))
					{
						if ( !indexAtTargetSetFlag) {
							indexAtTarget = stackIndex;
							indexAtTargetSetFlag = true;
							
							String message = String.format(
									"Guessed pick-up at StackTraceElement[ %1$d]: %2$s%n",
									stackIndex,
									stackTraceElementArray[ stackIndex].toString()
									);
							
							stackTraceElementStringList.add( message);
							if ( stackTraceElementStringList.size() >= count) break; // for
							continue;
						}
					}
					if ( stackIndex > indexAtTarget) {
						if ( 
								className.startsWith( "sun.reflect") 
								|| className.startsWith( "java.lang.reflect")
								)
						{
							continue;
						}
						// Store stacked invocation info before reflective invocation 
						// in stackTraceElementArray into stackTraceElementStringBuilder 
						String message 
						= String.format(
								"Guessed pick-up at StackTraceElement[ %1$d]: %2$s%n",
								stackIndex,
								stackTraceElementArray[ stackIndex].toString()
								);
						stackTraceElementStringList.add( message);
						if ( stackTraceElementStringList.size() >= count) break; // for
					}
				} // for
			}
			else {
				// this-object might have been destroyed if another same class with one being advised in class path 
				Logger logger = Util.getLogger();
				if ( logger.isLoggable( Level.WARNING)) {
					logger.warning( 
							String.format(
									"Null was returned by getThis method of JoinPoint on %1$s.%n" 
									+ "It may be just because the join point was on object with static modifier. " 
									+ "Or it may be caused by the existence of another same class not " 
									+ "advised in the class path. Please check if suspicious on later case.",
									joinPoint.toString()
									)
							);
				}
			}
		}
		
		return stackTraceElementStringList;
	} // public static List<String> getCallingMethodInfo( final StackTraceElement[] stackTraceElementArray, JoinPoint joinPoint, int count)
	
	/**
	 * Get information of target type, signature, arguments, and caller method from 
	 * <code>joinPoint</code> input.
	 * @param joinPoint to get information from.
	 * @param enclosingStaticPart should be the enclosing static part of 
	 * <code>joinPoint</code> input, or null.
	 * @return String containing information of target type, signature, arguments, and caller method 
	 * from <code>joinPoint</code> input in aligned format. 
	 */
	public static String getJoinPointInfo( 
			final JoinPoint joinPoint, final JoinPoint.EnclosingStaticPart enclosingStaticPart) {
		
		// Dump arguments -------------------------------------------------------------------------
		String argsStr = "no arguments";
			Object[] argumentsObjArray = joinPoint.getArgs();
			if ( argumentsObjArray.length > 0) {
				argsStr = Arrays.toString( argumentsObjArray);
			}
		// ----------------------------------------------------------------------------------------
		
		String joinPointSignatureStr = joinPoint.getSignature().toString();
		
		String joinPointInfo 
		= String.format(
				"Type: %1$s%n" +
				"Signature: %2$s%n" +
				"Arguments: %3$s",
				( (joinPoint.getTarget() == null) 
						? "no type info" : joinPoint.getTarget().getClass().getName()),
				joinPointSignatureStr,
				argsStr
				);
		
		// Add calling method infomration ---------------------------------------------------------
		if (  
				enclosingStaticPart != null 
				&& !joinPoint.getSourceLocation().toString().equals( 
						enclosingStaticPart.getSourceLocation().toString())
				) 
		{ // Use enclosingStaticPart to get calling method infomration
			joinPointInfo = String.format(
					"%1$s%n" +
					"Called from %2$s (%3$s)",
					joinPointInfo, 
					((CodeSignature)enclosingStaticPart.getSignature()).toString(),
					enclosingStaticPart.getSourceLocation().toString()
					);
		}
		else { // Use stack-trace to get calling method information
			String callingMethodInfo = "";
			StackTraceElement[] stackTraceElementArray = Thread.currentThread().getStackTrace();
			
			for( String stackTraceElementString : getCallingMethodInfo( stackTraceElementArray, joinPoint, 2)) {
				callingMethodInfo = String.format( 
						"%1$s" +
						"%2$c%3$s",
						callingMethodInfo, 
						'\t', 
						stackTraceElementString
						);
			} // for
			
			joinPointInfo = String.format(
					"%1$s%n" +
					"Called from:%n %2$s",
					joinPointInfo, 
					callingMethodInfo
					);
		}
		// ----------------------------------------------------------------------------------------
		
		return joinPointInfo;
	} // public static String getJoinPointInfo( JoinPoint joinPoint, JoinPoint.EnclosingStaticPart enclosingStaticPart)
}
